import * as THREE from "three"
import { LockViewmodeCommand, UnlockViewmodeCommand } from "../command/viewmode.command"
import CameraData from "../data/CameraData"
import CanvasData from "../data/CanvasData"
import MeshData from "../data/MeshData"
import SweepData from "../data/SweepData"
import ViewmodeData from "../data/ViewmodeData"
import transitionTypeEnum from "../enum/transitionType.enum"
import viewmodeEnum from "../enum/viewmode.enum"
import { ViewmodeActiveTransitionError, ViewmodeChangeLockedError } from "../exception/viewmode.exception"
import l from "../math/63560"
import b from "../math/69626"
import T from "../math/79437"
import { BeginSwitchViewmodeMessage, EndSwitchViewmodeMessage, SwitchViewmodeMessage } from "../message/viewmode.message"
import SymbolList from "../SymbolList"
import Matrix from "../three/Matrix"
import Vectors from "../three/Vectors"
import Logger from "../utils/Logger"
import { isPanOrMesh } from "../utils/viewmode.util"
import CameraModule from "./CameraModule"
import Module from "./Module"
import SweepDataModule from "./SweepDataModule"

const w = {
  rotationDuration: 1,
  rotationDelay: 0,
  matrixDelay: 0,
  matrixDuration: 0.5
}
const _ = {
  [viewmodeEnum.Dollhouse]: { matrixDelay: 0.5, matrixDuration: 1 },
  [viewmodeEnum.Panorama]: { rotationDuration: 0.5 }
}
const O = new Logger("viewmodes-data")
const D = 1000
const C = Math.PI / 2 + Number.EPSILON
const P = Math.PI - Number.EPSILON
const I = {
  [viewmodeEnum.Panorama]: D,
  [viewmodeEnum.Dollhouse]: D,
  [viewmodeEnum.Floorplan]: D,
  [viewmodeEnum.Orthographic]: D
}
export default class ViewmodeModule extends Module {
  viewmodeData: ViewmodeData
  minDollhouseDistance: number
  maxDollhouseDistance: number
  _orthoMatrix: Matrix
  sweepData: any
  meshData: any
  canvas: any
  cameraData: any
  engine: any
  market: any
  cameraModule: any
  sweepModule: any
  constructor() {
    super(...arguments), (this.viewmodeData = new ViewmodeData())
    this.name = "viewmode"
    this.minDollhouseDistance = 15
    this.maxDollhouseDistance = 50
    this._orthoMatrix = new Matrix()
  }
  getFlyinEndPose(e) {
    const t = e.position || this.sweepData.getSweep(e.sweepID).position,
      i = e.rotation || new THREE.Quaternion(),
      n = (Math.PI / 180) * l.oR.fov
    return b.YN(t, i, -Math.PI / 6, this.meshData.bounds, n, this.canvas.aspectRatio)
  }
  getFlyinStartPose(e, t) {
    const i = t || this.cameraData.pose.position,
      n = e.position.y - i.y,
      s = b.Ll(e.position, i, this.meshData.center),
      r = Vectors.FORWARD.clone().applyQuaternion(s)
    return {
      position: b.D7(e.position, i, (Math.sign(r.y) * Math.PI) / 4, !0).setY(1.5 * n)
    }
  }
  async init(e, t) {
    ;(this.engine = t),
      (this.market = t.market),
      null !== e.startingMode && (this.viewmodeData.currentMode = e.startingMode),
      this.market.register(this, ViewmodeData, this.viewmodeData),
      t.market.waitForData(MeshData).then(e => {
        ;(this.meshData = e), this.setDollhouseBounds()
      }),
      this.bindings.push(
        t.commandBinder.addBinding(LockViewmodeCommand, this.lockViewmode.bind(this)),
        t.commandBinder.addBinding(UnlockViewmodeCommand, this.unlockViewmode.bind(this))
      )
    const [i, n, s, r, o] = await Promise.all([
      t.getModule(CameraModule),
      t.getModule(SweepDataModule),
      t.market.waitForData(SweepData),
      t.market.waitForData(CanvasData),
      t.market.waitForData(CameraData)
    ])
    ;(this.cameraModule = i), (this.sweepModule = n), (this.sweepData = s), (this.canvas = r), (this.cameraData = o)
  }
  lockViewmode() {
    ;(this.viewmodeData.viewmodeChangeEnabled = !1), this.viewmodeData.commit()
  }
  unlockViewmode() {
    ;(this.viewmodeData.viewmodeChangeEnabled = !0), this.viewmodeData.commit()
  }
  get currentMode() {
    return this.viewmodeData.currentMode
  }
  async switchToMode(e, t = transitionTypeEnum.FadeToBlack, i?, o?) {
    if (
      (O.info(`switchToMode mode:${e} type:${t}`),
      this.meshData || (e !== viewmodeEnum.Floorplan && e !== viewmodeEnum.Dollhouse) || (await this.engine.getModuleBySymbol(SymbolList.MODEL_MESH)),
      t === transitionTypeEnum.Instant && (o = 0),
      (o = void 0 !== o ? o : I[e]),
      !this.viewmodeData.canStartTransition() || !this.cameraData.canTransition())
    )
      throw new ViewmodeActiveTransitionError()
    if (e !== this.currentMode && !this.viewmodeData.viewmodeChangeEnabled) throw new ViewmodeChangeLockedError()
    const a = this.isAlreadyInMode(this.viewmodeData, e),
      l = i && !ViewmodeModule.willPoseChange(this.cameraData, i)
    if (a && l) return Promise.resolve()
    this.sweepData.currentSweep && this.sweepData.isSweepUnaligned(this.sweepData.currentSweep) && !isPanOrMesh(e) && (t = transitionTypeEnum.FadeToBlack)
    const c = null !== this.viewmodeData.currentMode ? this.viewmodeData.currentMode : void 0
    this.viewmodeData.activateTransition(this.viewmodeData.currentMode, e, o),
      (this.viewmodeData.currentMode = viewmodeEnum.Transition),
      this.viewmodeData.commit(),
      this.engine.broadcast(new BeginSwitchViewmodeMessage(e, c))
    const h = this.moveCameraToViewmode(e, c, t, o, i)
    let d = !1
    return (
      h.progress(t => {
        ;(this.viewmodeData.transition.progress = t),
          this.viewmodeData.commit(),
          t >= 0.5 && !d && ((d = !0), this.engine.broadcast(new SwitchViewmodeMessage(e, c)))
      }),
      h.then(() => {
        ;(this.viewmodeData.transition.progress = 1),
          (this.viewmodeData.currentMode = e),
          this.viewmodeData.deactivateTransition(),
          this.viewmodeData.commit(),
          this.engine.broadcast(new EndSwitchViewmodeMessage(e, c))
      }),
      h.nativePromise()
    )
  }
  isAlreadyInMode(e, t) {
    const i = e.transition.from === e.transition.to,
      n = e.currentMode === t
    return (e.transition.active && i) || n
  }
  static willPoseChange(e, t) {
    return (
      (void 0 !== t.position && !e.pose.position.equals(t.position)) ||
      (void 0 !== t.rotation && !e.pose.rotation.equals(t.rotation)) ||
      (void 0 !== t.zoom && e.zoom() !== t.zoom)
    )
  }
  moveCameraToViewmode(e, t, i, n, r) {
    switch (e) {
      case viewmodeEnum.Mesh:
      case viewmodeEnum.Panorama:
        return this.moveCameraToPanorama(i, n, r)
      case viewmodeEnum.Dollhouse:
        return this.moveCameraToDollhouse(t, i, n, r)
      case viewmodeEnum.Floorplan:
        return this.moveCameraToFloorplan(t, i, n, r)
      case viewmodeEnum.Orthographic:
        return this.moveCameraToOrthographic(t, i, n, r)
      default:
        throw Error(`moveCameraToViewmode(): Invalid view-mode to go to: ${e}`)
    }
  }
  moveCameraToPanorama(e, t, i) {
    if (this.sweepData.canTransition() || this.cameraData.canTransition()) {
      let n
      if (i && i.sweepID) n = i.sweepID
      else {
        const e = this.sweepData.getFirstSweep()
        ;(n = this.sweepData.currentSweep), !n && e && (n = e.id)
      }
      if (!n) throw new Error("moveCameraToPanorama(): Invalid target sweep!")
      this.sweepData.isSweepUnaligned(n) && (e = transitionTypeEnum.FadeToBlack)
      let l = i && i.rotation ? i.rotation : void 0
      if (!l) {
        const e = (this.viewmodeData.transition.from === viewmodeEnum.Floorplan ? Vectors.UP : Vectors.FORWARD)
          .clone()
          .applyQuaternion(this.cameraData.pose.rotation)
          .setY(0)
          .normalize()
        l = new THREE.Quaternion().setFromUnitVectors(Vectors.FORWARD, e)
      }
      return this.sweepModule.moveToSweep({
        transitionType: e,
        sweepId: n,
        rotation: l,
        transitionTime: t,
        rotationDelay: 0.5
      })
    }
    throw new Error("Could not move to panorama mode because another transition was active")
  }
  moveCameraToDollhouse(e, t, i, n) {
    const r = this.meshData.bounds,
      c: any = {}
    let h
    if (n && (n.position || n.rotation)) {
      if (n.position && n.rotation) {
        ;(c.position = n.position), (c.rotation = n.rotation)
        const e = new THREE.Plane(Vectors.UP.clone(), this.meshData.center.y)
        let t = b.Fe(c.position, c.rotation, e)
        t || ((t = new THREE.Vector3()), t.copy(this.meshData.center)), r.containsPoint(t) && (h = c.position.distanceTo(t))
      } else if (n.position) (c.position = n.position), (c.rotation = b.n0(n.position, this.meshData.center))
      else {
        c.rotation = n.rotation
        const e = r.min.distanceTo(r.max)
        c.position = b.fd(c.rotation, this.meshData.center, e)
      }
      h || (h = this.meshData.center.clone().distanceTo(c.position))
    } else {
      let e, t
      const i = this.sweepData.isSweepUnaligned(this.sweepData.currentSweep),
        n = i || this.viewmodeData.transition.from === viewmodeEnum.Floorplan ? this.meshData.center : this.cameraData.pose.position
      if (i) (t = new THREE.Vector3(0, 0, 0)), (e = new THREE.Vector3(15, 10, 15))
      else if (this.viewmodeData.transition.from !== viewmodeEnum.Floorplan) {
        const i = 1.5,
          s = b.YN(
            this.cameraData.pose.position,
            this.cameraData.pose.rotation,
            -Math.PI / 6,
            this.meshData.bounds,
            this.cameraData.fovY() * i,
            this.cameraData.pose.aspect()
          ),
          r = ((e, t) => {
            const i = e.clone().add(new THREE.Vector3(0, 6, 0)),
              n = Vectors.FORWARD.clone().applyQuaternion(t)
            return (
              n.y >= 0.01 && (n.y = 0.01),
              {
                position: i.clone().add(n.clone().multiplyScalar(-10)),
                direction: n
              }
            )
          })(n, this.cameraData.pose.rotation)
        ;[e, t] = [s.position, r.direction]
      } else {
        const i = ((e, t) => {
          const i = e.clone(),
            n = Vectors.DOWN.clone().applyQuaternion(t).multiplyScalar(6),
            s = i.clone().add(n).setY(6),
            r = Vectors.UP.clone().applyQuaternion(t)
          return r.setY(-0.5).normalize(), { position: s, direction: r }
        })(n, this.cameraData.pose.rotation)
        ;[e, t] = [i.position, i.direction]
      }
      const r = n.clone().add(t).setY(this.meshData.center.y)
      ;(c.position = b.r0(e, r, this.minDollhouseDistance, this.maxDollhouseDistance)),
        (c.position = b.m$(c.position, r, C, P)),
        (c.rotation = b.n0(c.position, r)),
        (h = r.distanceTo(c.position))
    }
    return this.cameraModule.moveTo({
      transitionType: t,
      pose: c,
      transitionTime: i,
      projection: l.AN(this.cameraData.aspect()),
      focalDistance: h,
      rotationDuration: 0.5
    })
  }
  setDollhouseBounds() {
    const e = this.meshData.bounds,
      t = e.min.distanceTo(e.max)
    ;(this.minDollhouseDistance = Math.min(t / 2, this.minDollhouseDistance)), (this.maxDollhouseDistance = Math.max(t, this.maxDollhouseDistance))
  }
  moveCameraToFloorplan(e, t, i, n) {
    const s: any = {},
      r = this.meshData.center,
      c = this.meshData.size,
      h = Math.max(c.x, c.z),
      d = this.canvas.aspectRatio,
      u = this.getDefaultFloorplanZoom(),
      p = n && n.zoom ? this.canvas.height / (2 * n.zoom) : u,
      m = l.aH(this.canvas.width, this.canvas.height, p),
      g = l.aH(this.canvas.width, this.canvas.height, u)
    if ((this.cameraModule.setBaseProjection(g), n && n.position)) s.position = n.position
    else {
      const e = Vectors.UP.clone().multiplyScalar(h)
      s.position = r.clone().add(e)
    }
    if (n && n.rotation) s.rotation = n.rotation
    else {
      const e = new Matrix().setPosition(s.position)
      e.lookAt(s.position, r, Vectors.FORWARD), (s.rotation = new THREE.Quaternion().setFromRotationMatrix(e.asThreeMatrix4()))
      if (d < 1 !== c.x / c.z < 1) {
        const e = new THREE.Quaternion().setFromUnitVectors(Vectors.UP, Vectors.LEFT)
        s.rotation.multiply(e)
      }
    }
    const f = s.position.y - this.meshData.center.y
    let v = Object.assign(Object.assign({}, w), {
      easing: T.hl,
      transitionType: t,
      pose: s,
      transitionTime: i,
      projection: m,
      focalDistance: f
    })
    return e && e in _ && (v = Object.assign(Object.assign({}, _[e]), v)), this.cameraModule.moveTo(v)
  }
  moveCameraToOrthographic(e, t, i, n) {
    const s = n && n.zoom ? n.zoom : this.getDefaultFloorplanZoom(),
      r = l.aH(this.canvas.width, this.canvas.height, s, this._orthoMatrix)
    this.cameraModule.setBaseProjection(r)
    const o = {
      easing: T.hl,
      transitionType: t,
      pose: n || {},
      transitionTime: i,
      projection: r,
      focalDistance: 2
    }
    return this.cameraModule.moveTo(o)
  }
  getDefaultFloorplanZoom() {
    const e = this.meshData.size,
      t = Math.max(e.x, e.z),
      i = Math.min(e.x, e.z),
      n = this.canvas.aspectRatio,
      s = Math.max(t, i / n),
      r = Math.max(i, t / n),
      o = n < 1 ? s : r
    return this.canvas.height / (1.2 * o)
  }
}
